昨天我們介紹了音訊串流 MediaStream 與 MediaStreamTrack,以及負責與裝置溝通的 MediaDevices。今天就來介紹與錄音有關的 MediaStream Recording API 吧!
這支 API 可以取出 MediaStream 的音訊資料,提供給開發者做進一步處理,像是分析波形頻率、調整聲音、儲存並上傳到遠端 server 等。其中最主要的 Interface 就只有一個:MediaRecorder。
它會將 MediaStream 內的音訊資料取出來,根據指定的每秒位元率、錄製好的檔案格式 MIME type 塞到內部的 Blobs 中。
const options = {
mimeType: 'audio/mpeg',
audioBitsPerSecond: 44100*16,
}
const recorder = new MediaRecorder(stream, options)
我們可以決定錄製的時候,是將整個 MediaStream 錄成一個 Blob,也可以分成每 N 秒一個 Blob。
const timeslice = 1000
recorder.start(timeslice)
有三個時機點可以取得錄製好的 Blob 資料:
let newTrack = [];
recorder.addEventListener('dataavailable', (e) => {
if (e.data.size > 0) {
newTrack.push(e.data);
}
})
recorder.addEventListener('stop', () => console.log('Record Success!'));
name | description | param | return |
---|---|---|---|
MediaRecorder() | constructor,帶入 MediaStream 並建立一個 MediaRecorder 物件,可以設定 options 決定 MediaRecorder 的 mimeType 與每秒位元率 | stream (req.), options (opt.) | MediaRecorder 物件 |
isTypeSupported() | 測試是否支援錄製此 MIME type | mimeType 字串 | boolean |
pause() | 暫停錄製,將 recording 狀態轉為 pause,Blob 資料保留。若原本非 recording 狀態,會得到 InvalidState Error | none | undefined |
resume() | 繼續錄製,將 pause 狀態轉為 recording 並觸發 resume 事件。若原本非 pause 狀態,會得到 InvalidState Error | none | undefined |
start() | 開始錄製,將 inactive 狀態轉為 recording 並觸發 start 事件。可指定 timeslice (ms) 每 N 秒分割一個 Blob、或是從頭錄到尾都同一個 Blob。若原本非 inactive 狀態,會得到 InvalidState Error | timeslice (opt.) | undefined |
stop() | 停止錄製,將 recording 狀態轉為 inactive 並觸發 stop 與 dataavailable 事件。若原本非 recording 狀態,會得到 InvalidState Error | none | undefined |
requestData() | 手動觸發 dataavailable 事件以便取得資料 chunk |
name | description | value |
---|---|---|
ignoreMutedMedia | 決定當 MediaStreamTrack 是靜音 (muted) 時是否要錄下資料 | boolean (default: false) |
audioBitsPerSecond | Read only. 取得每秒將多少位元的音訊資料存入 Blob 的數值 | number |
videoBitsPerSecond | Read only. 取得每秒將多少位元的影片資料存入 Blob 的數值 | number |
mimeType | Read only. 取得錄製完成後要儲存的 file format | mimeType string |
state | Read only. 取得當前 MediaRecorder 的狀態 | inactive (錄製停止), recording (錄製中), paused (錄製暫停) |
stream | Read only. 取得 MediaStream | MediaStream |
trigger | trigger events | event handlers |
---|---|---|
pause() | pause | onpause |
resume() | resume | onresume |
start() | start | onstart |
stop() | stop -> dataavailable | onstop, ondataavailable |
requestData() | dataavailable | ondataavailable |
exceptions | error | onerror |
接下來介紹不同情境下,實際的錄製方法吧!
最簡單的使用方式。
當使用者點擊 <input>
時,桌機會請使用者上傳音檔、行動裝置可能會有選項讓使用者用麥克風錄音,錄完後上傳音檔,再進行分析處理。這種情況下可以不用用到 <audio>
,甚至也不需要用到 MediaRecorder,只需要從 <input>
取出檔案即可處理。
<input type="file" accept="audio/*" capture="microphone" id="recorder">
const fileUploader = document.getElementById('recorder');
fileUploader.addEventListener('change', function(e) {
const file = e.target.files[0];
// 針對錄製檔案的處理 ...
});
有兩種方式可以取得遠端音檔:
<audio>
、<video>
)因為還沒介紹到 Web Audio API,先講第一種方式該怎麼錄製。步驟如下:
<audio>
設定 src
屬性讀取遠端檔案<audio>
內的音源轉成 MediaStreamconst remoteUrl = '.....';
const timeslice = 10000;
let chunks = [];
const myAudio = new Audio(remoteUrl);
const recorder = new MediaRecorder(myAudio.captureStream(), { mimeType: 'audio/mpeg' });
recorder.ondataavailable(e => {
if (e.data.size > 0) chunks.push(e.data);
// 針對每個 chunk 的處理 ...
});
recorder.onstop(() => {
// 針對錄製檔案的處理 ...
});
// 播放的同時一起錄製
myAudio.addEventListener('play', () => {
if (recorder.state === 'inactive') {
recorder.start(timeslice);
} else if (recorder.state === 'pause') {
recorder.resume();
}
});
myAudio.addEventListener('pause', () => recorder.pause());
myAudio.addEventListener('stop', () => recorder.stop());
myAudio.play();
當然,也可以不用播放 <audio>
,讀取完畢後直接背景錄製也行。
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// 取得麥克風裝置
const deviceId = navigator.mediaDevices.enumerateDevices()
.then(devices => devices.find(deviceInfo => deviceInfo.kind === 'audioinput'))
.catch(e => console.error(e))
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
const contraints = {
audio: {
deviceId,
sampleRate: 44100,
sampleSize: 16,
}
}
if (!deviceId) return;
// 取得麥克風資料
navigator.mediaDevices.getUserMedia(contraints)
.then(stream => {
const options = { mimeType: 'audio/mpeg' };
const recordedChunks = [];
const recorder = new MediaRecorder(stream, options);
recorder.ondataavailable = e => {
if (e.data.size > 0) recordedChunks.push(e.data);
// 針對每個 chunk 的處理 ...
});
recorder.onstop = () => {
// 針對錄製檔案的處理 ...
});
recorder.start();
});
}
順道一提,有一個進行中的 W3C 標準:Audio Output Devices API,它希望擴充 HTMLMediaElemet 功能,可以透過 audio.setSinkId(deviceId)
選擇音檔播放的硬體裝置,看起來 Chrome、Edge 有實作、Firefox 還沒,再讓子彈飛一會吧~
在 Google 的這篇教學文章有提到:getUserMedia() 會在使用者第一次訪問網站時,跳出提示請使用者授予麥克風的存取權限。往往這種授權提示一般人第一時間會傾向不允許,但是一旦拿不到麥克風的存取權限,我們也沒辦法從 getUserMedia() 知道這件事。這時候可以透過 Permission API 處理這件事。
要注意的是,Permission API 目前還只是 W3C 的 Working Draft,可以在 Chrome 和 Firefox 上運行,但其他瀏覽器尚未支援,請參考 caniuse。
navigator.permissions.query({name:'microphone'}).then(function(result) {
if (result.state == 'granted') {
// 已授予對麥克風的訪問權
} else if (result.state == 'prompt') {
// 尚未授予訪問權,調用 getUserMedia 時將會收到提示
} else if (result.state == 'denied') {
// 系統或用戶已拒絕對麥克風的訪問權
}
result.onchange = function() {
// 授權有變化時的處理 ...
};
});
今天就到這邊啦!
明天開始會進入另一個實作,會應用到這段時間的所學,敬請期待吧~